#!/bin/sh
# tlp-func-gpu - Intel GPU Functions
#
# Copyright (c) 2024 Thomas Koch <linrunner at gmx.net> and others.
# SPDX-License-Identifier: GPL-2.0-or-later

# Needs: tlp-func-base

# ----------------------------------------------------------------------------
# Constants

readonly BASE_MODD=/sys/module
readonly BASE_DRMD=/sys/class/drm
readonly BASE_DEBUGD=/sys/kernel/debug/dri

readonly IGPU_MIN_FREQ=gt_min_freq_mhz
readonly IGPU_MAX_FREQ=gt_max_freq_mhz
readonly IGPU_BOOST_FREQ=gt_boost_freq_mhz
# shellcheck disable=SC2034
readonly IGPU_RPN_FREQ=gt_RPn_freq_mhz
# shellcheck disable=SC2034
readonly IGPU_RP0_FREQ=gt_RP0_freq_mhz

# ----------------------------------------------------------------------------
# Functions

# --- Intel GPU

get_intel_gpu_sysdirs () {
    # determine Intel GPU sysdirs
    # $1: drm sysdir, $2: driver
    # retval: $_intel_gpu_parm: parameter sysdir;
    #         $_intel_gpu_dbg:  debug sysdir

    _intel_gpu_parm=${BASE_MODD}/$2/parameters
    _intel_gpu_dbg=${BASE_DEBUGD}/${1##"${BASE_DRMD}/card"}
    echo_debug "pm" "get_intel_gpu_sysdirs: gpu=$1 driver=$2; parm=$_intel_gpu_parm; dbg=$_intel_gpu_dbg"

    return 0
}

set_intel_gpu_min_max_boost_freq () {
    # set gpu frequency limits
    # $1: 0=ac mode, 1=battery mode
    # rc: 0=ok/1=parameter error
    local new_min new_max new_boost
    local old_min old_max old_boost gpu_min gpu_max
    local driver suffix

    if [ "$1" = "1" ]; then
        new_min=${INTEL_GPU_MIN_FREQ_ON_BAT:-}
        new_max=${INTEL_GPU_MAX_FREQ_ON_BAT:-}
        new_boost=${INTEL_GPU_BOOST_FREQ_ON_BAT:-}
        suffix="BAT"
    else
        new_min=${INTEL_GPU_MIN_FREQ_ON_AC:-}
        new_max=${INTEL_GPU_MAX_FREQ_ON_AC:-}
        new_boost=${INTEL_GPU_BOOST_FREQ_ON_AC:-}
        suffix="AC"
    fi

    if [ -z "$new_min" ] && [ -z "$new_max" ] && [ -z "$new_boost" ]; then
        echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).not_configured"
        return 0
    fi

    for gpu in "${BASE_DRMD}"/card?; do
        driver=$(readlink "${gpu}/device/driver")
        driver=${driver##*/}
        case "$driver" in
            i915*) # Intel GPU found
                get_intel_gpu_sysdirs "$gpu" "$driver"

                # shellcheck disable=SC2034
                if old_min=$(read_sysf "$gpu/$IGPU_MIN_FREQ") \
                    && old_max=$(read_sysf "$gpu/$IGPU_MAX_FREQ") \
                    && old_boost=$(read_sysf "$gpu/$IGPU_BOOST_FREQ") \
                    && gpu_min=$(read_sysf "$gpu/$IGPU_RPN_FREQ") \
                    && gpu_max=$(read_sysf "$gpu/$IGPU_RP0_FREQ"); then
                    # frequencies actually readable, check new ones against hardware limits and boundary conditions
                    if ! is_uint "$new_min" 5 || [ "$new_min" -lt "$gpu_min" ] || [ "$new_min" -gt "$gpu_max" ]; then
                        echo_message "Error in configuration at INTEL_GPU_MIN_FREQ_ON_${suffix}=\"${new_min}\": frequency invalid or out of range (see 'tlp-stat -g')."
                        echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).invalid: gpu=$gpu min=$new_min gpu_min=$gpu_min hw_max=$gpu_max; rc=1"
                        return 1
                    elif ! is_uint "$new_max" 5 || [ "$new_max" -lt "$gpu_min" ] || [ "$new_max" -gt "$gpu_max" ]; then
                        echo_message "Error in configuration at INTEL_GPU_MAX_FREQ_ON_${suffix}=\"${new_max}\": frequency invalid or out of range (see 'tlp-stat -g')."
                        echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).invalid: gpu=$gpu min=$new_min gpu_min=$gpu_min gpu_max=$gpu_max; rc=1"
                        return 1
                    elif ! is_uint "$new_boost" 5 || [ "$new_boost" -lt "$gpu_min" ] || [ "$new_boost" -gt "$gpu_max" ]; then
                        echo_message "Error in configuration at INTEL_GPU_BOOST_FREQ_ON_${suffix}=\"${new_boost}\": frequency invalid or out of range (see 'tlp-stat -g')."
                        echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).invalid: gpu=$gpu boost=$new_boost gpu_min=$gpu_min gpu_max=$gpu_max; rc=1"
                        return 1
                    elif [ "$new_min" -gt "$new_max" ]; then
                        echo_message "Error in configuration: INTEL_GPU_MIN_FREQ_ON_${suffix} > INTEL_GPU_MAX_FREQ_ON_${suffix}."
                        echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).min_gt_max: gpu=$gpu min=$new_min max=$new_max; rc=1"
                        return 1
                    elif [ "$new_max" -gt "$new_boost" ]; then
                        echo_message "Error in configuration: INTEL_GPU_MAX_FREQ_ON_${suffix} > INTEL_GPU_BOOST_FREQ_ON_${suffix}."
                        echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).max_gt_boost: gpu=$gpu max=$new_max boost=$new_boost; rc=1"
                        return 1
                    fi

                    # all parameters valid --> write min, max in proper sequence
                    if [ "$new_min" -gt "$old_max" ]; then
                        write_sysf "$new_max" "$gpu/$IGPU_MAX_FREQ"
                        echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).max: gpu=$gpu freq=$new_max; rc=$?"
                        write_sysf "$new_min" "$gpu/$IGPU_MIN_FREQ"
                        echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).min: gpu=$gpu freq=$new_min; rc=$?"
                    else
                        write_sysf "$new_min" "$gpu/$IGPU_MIN_FREQ"
                        echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).min: gpu=$gpu freq=$new_min; rc=$?"
                        write_sysf "$new_max" "$gpu/$IGPU_MAX_FREQ"
                        echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).max: gpu=$gpu freq=$new_max; rc=$?"
                    fi
                    write_sysf "$new_boost" "$gpu/$IGPU_BOOST_FREQ"
                    echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).boost: gpu=$gpu freq=$new_boost; rc=$?"
                else
                    echo_debug "pm" "set_intel_gpu_min_max_boost_freq($1).not_available: gpu=$gpu"
                fi
        esac
    done

    return 0
}

# --- AMD Radeon GPU

set_amdgpu_profile () {
    # set amdgpu/radeon power profile
    # $1: 0=ac mode, 1=battery mode

    local driver gpu level pwr rc1 rc2
    local sdone=0 # 1=gpu present

    for gpu in "${BASE_DRMD}"/card?; do
        driver=$(readlink "${gpu}/device/driver")
        driver=${driver##*/}
        case "$driver" in
            amdgpu)
                if [ -f "$gpu/device/power_dpm_force_performance_level" ]; then
                    # Use amdgpu dynamic power management method (DPM)
                    if [ "$1" = "1" ]; then
                        level=${RADEON_DPM_PERF_LEVEL_ON_BAT:-}
                    else
                        level=${RADEON_DPM_PERF_LEVEL_ON_AC:-}
                    fi

                    if [ -z "$level" ]; then
                        # do nothing if unconfigured
                        echo_debug "pm" "set_amdgpu_profile($1).amdgpu.not_configured: gpu=$gpu"
                        return 0
                    else
                        write_sysf "$level" "$gpu/device/power_dpm_force_performance_level"; rc1=$?
                        echo_debug "pm" "set_amdgpu_profile($1).amdgpu: gpu=$gpu level=${level}: rc=$rc1"
                    fi
                    sdone=1
                fi
                ;;

            radeon)
                if [ -f "$gpu/device/power_dpm_force_performance_level" ] && [ -f "$gpu/device/power_dpm_state" ]; then
                    # Use radeon dynamic power management method (DPM)
                    if [ "$1" = "1" ]; then
                        level=${RADEON_DPM_PERF_LEVEL_ON_BAT:-}
                        pwr=${RADEON_DPM_STATE_ON_BAT:-}
                    else
                        level=${RADEON_DPM_PERF_LEVEL_ON_AC:-}
                        pwr=${RADEON_DPM_STATE_ON_AC:-}
                    fi

                    if [ -z "$pwr" ] || [ -z "$level" ]; then
                        # do nothing if (partially) unconfigured
                        echo_debug "pm" "set_amdgpu_profile($1).radeon.not_configured: gpu=$gpu"
                        return 0
                    else
                        write_sysf "$level" "$gpu/device/power_dpm_force_performance_level"; rc1=$?
                        write_sysf "$pwr" "$gpu/device/power_dpm_state"; rc2=$?
                        echo_debug "pm" "set_amdgpu_profile($1).radeon: gpu=$gpu perf=${level}: rc=$rc1; state=${pwr}: rc=$rc2"
                    fi
                    sdone=1

                elif [ -f "$gpu/device/power_method" ] && [ -f "$gpu/device/power_profile" ]; then
                    # Use legacy radeon power profile method
                    if [ "$1" = "1" ]; then
                        pwr=${RADEON_POWER_PROFILE_ON_BAT:-}
                    else
                        pwr=${RADEON_POWER_PROFILE_ON_AC:-}
                    fi

                    if [ -z "$pwr" ]; then
                        # do nothing if unconfigured
                        echo_debug "pm" "set_amdgpu_profile($1).radeon_legacy.not_configured: gpu=$gpu"
                        return 0
                    else
                        write_sysf "profile" "$gpu/device/power_method"; rc1=$?
                        write_sysf "$pwr" "$gpu/power_profile"; rc2=$?
                        echo_debug "pm" "set_amdgpu_profile($1).radeon_legacy: gpu=$gpu method=profile: rc=$rc1; profile=${pwr}: rc=$rc2"
                    fi
                    sdone=1
                fi
                ;;
        esac
    done

    if [ $sdone -eq 0 ]; then
        echo_debug "pm" "set_amdgpu_profile($1).no_gpu"
    fi

    return 0
}

set_abm_level () {
    # set amdgpu adaptive backlight modulation (ABM)
    # $1: 0=ac mode, 1=battery mode

    local card driver gpu level pps rc
    local sdone=0 # 1=gpu present

    for gpu in "${BASE_DRMD}"/card?; do
        driver=$(readlink "${gpu}/device/driver")
        driver=${driver##*/}
        case "$driver" in
            amdgpu)
                card="${gpu##/*/}"
                for pps in "$gpu/${card}-eDP"*; do
                    if [ -f "$pps/amdgpu/panel_power_savings" ]; then
                        if [ "$1" = "1" ]; then
                            level=${AMDGPU_ABM_LEVEL_ON_BAT:-}
                        else
                            level=${AMDGPU_ABM_LEVEL_ON_AC:-}
                        fi

                        if [ -z "$level" ]; then
                            # do nothing if unconfigured
                            echo_debug "pm" "set_abm_level($1).amdgpu.not_configured: gpu=$gpu"
                            return 0
                        elif check_ppd_active ; then
                            # don't apply ABM when power-profiles-daemon is running
                            echo_message "Warning: AMDGPU_ABM_LEVEL_ON_AC/BAT is not set because power-profiles-daemon is running."
                            echo_debug "pm" "set_abm_level($1).amdgpu.nop_ppd_active"
                            return 0
                        else
                            write_sysf "$level" "$pps/amdgpu/panel_power_savings"; rc=$?
                            echo_debug "pm" "set_abm_level($1).amdgpu: pps=$pps level=${level}: rc=$rc"
                        fi
                        sdone=1
                    fi
                done
                ;;

        esac
    done

    if [ $sdone -eq 0 ]; then
        echo_debug "pm" "set_abm_level($1).no_gpu_or_abm"
    fi

    return 0
}
